package slap.server;

import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CACHE_CONTROL;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.COOKIE;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ETAG;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.HOST;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.IF_MODIFIED_SINCE;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.IF_NONE_MATCH;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.LAST_MODIFIED;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.ORIGIN;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY1;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_KEY2;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_LOCATION;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_ORIGIN;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SEC_WEBSOCKET_PROTOCOL;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SERVER;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.SET_COOKIE;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_LOCATION;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_ORIGIN;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.WEBSOCKET_PROTOCOL;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import jodd.io.FastByteArrayOutputStream;
import jodd.io.StreamUtil;
import jodd.util.StringUtil;
import jodd.util.idgen.Uuid32Generator;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.handler.codec.http.CookieDecoder;
import org.jboss.netty.handler.codec.http.CookieEncoder;
import org.jboss.netty.handler.codec.http.DefaultCookie;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
import org.jboss.netty.handler.codec.http.HttpMessage;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.websocket.DefaultWebSocketFrame;
import org.jboss.netty.handler.codec.http.websocket.WebSocketFrame;
import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameDecoder;
import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameEncoder;
import org.jboss.netty.handler.stream.ChunkedFile;
import org.jboss.netty.handler.stream.ChunkedInput;
import org.jboss.netty.handler.stream.ChunkedStream;
import org.perf4j.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import slap.Slap;
import slap.concurrent.Invoker;
import slap.concurrent.Promise;
import slap.exception.UnexpectedException;
import slap.http.Cookie;
import slap.http.Header;
import slap.http.Request;
import slap.http.Response;
import slap.http.websocket.Inbound;
import slap.http.websocket.Outbound;
import slap.log.ACLogger;
import slap.mvc.scope.Session;
import slap.paradigm.Procedure;
import slap.paradigm.Procedure.Procedure1;
import slap.util.MimeTypes;
import slap.util.Perf4j;
import slap.util.Time;
import slap.util.Utils;

import app.blog.Blog;


public class FrameworkHandler extends SimpleChannelUpstreamHandler {
	
	protected static final Logger logger = LoggerFactory.getLogger(FrameworkHandler.class);
	
	public static ChannelGroup channelGroup = new DefaultChannelGroup();
	
	private static final String signature = "Slap Fronted WS[NTY]:" + Slap.VERSION;
	private static final boolean exposePlayServer = true;

	private static final Pattern IP_PATTERN = Pattern
			.compile("/[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+[:][0-9]+");

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
			throws Exception {
		if(e.getCause() instanceof java.io.IOException){
			
		}else{
			e.getCause().printStackTrace();
		}
	}

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
			throws Exception{
		Perf4j.start("fh");
		final Object msg = e.getMessage();
		if (msg instanceof HttpRequest) {

			final HttpRequest nettyRequest = (HttpRequest) msg;
			
			if (Values.UPGRADE.equalsIgnoreCase(nettyRequest
							.getHeader(Names.CONNECTION))
					&& Values.WEBSOCKET.equalsIgnoreCase(nettyRequest
							.getHeader(Names.UPGRADE))) {
				try {
					 websocketHandshake(ctx, nettyRequest, e);
				} catch (NoSuchAlgorithmException ae) {
					ae.printStackTrace();
				}
				return;
			}

			final Request request = new RequestNettyImpl(nettyRequest, ctx);
			final Response response = new Response();

			try {
				Response.current.set(response);
				response.out = new FastByteArrayOutputStream();
				response.direct = null;

				Cookie c = request.cookie(Session.COOKIE_SESSION_ID);

				if (c == null || StringUtil.isAllBlank(c.value)) {
					c = new Cookie();
					c.name = Session.COOKIE_SESSION_ID;
					c.value = String.valueOf(Uuid32Generator.generateUUID());
					c.maxAge = 1800;
					response.cookies.put(Session.COOKIE_SESSION_ID, c);
				}
				request.sessionId = c.value;
				Invoker.invoke(new HttpRequestInvokation(request, response, ctx,
						nettyRequest, e));
			} catch (Exception ex) {
				//TODO ser500
			}

		} else if (msg instanceof WebSocketFrame) {
			WebSocketFrame frame = (WebSocketFrame) msg;
			websocketFrameReceived(ctx, frame);
		}
	}


	static String getRemoteIPAddress(ChannelHandlerContext ctx) {
		String fullAddress = ((InetSocketAddress) ctx.getChannel()
				.getRemoteAddress()).getAddress().getHostAddress();
		if (IP_PATTERN.matcher(fullAddress).matches()) {
			fullAddress = fullAddress.substring(1);
			fullAddress = fullAddress.substring(0, fullAddress.indexOf(":"));
		} else if (fullAddress.matches(".*[%].*")) {
			fullAddress = fullAddress.substring(0, fullAddress.indexOf("%"));
		}
		return fullAddress;
	}

	protected static void addToResponse(Response response,
			HttpResponse nettyResponse) {
		Map<String, Header> headers = response.headers;
		for (Map.Entry<String, Header> entry : headers.entrySet()) {
			Header hd = entry.getValue();
			for (String value : hd.value.split(";")) {
				nettyResponse.setHeader(entry.getKey(), value);
			}
		}
		Map<String, Cookie> cookies = response.cookies;

		for (Cookie cookie : cookies.values()) {
			CookieEncoder encoder = new CookieEncoder(true);
			DefaultCookie c = new DefaultCookie(cookie.name, cookie.value);
			c.setSecure(cookie.secure);
			c.setPath(cookie.path);
			if (cookie.domain != null) {
				c.setDomain(cookie.domain);
			}
			if (cookie.maxAge != null) {
				c.setMaxAge(cookie.maxAge);
			}
			c.setHttpOnly(cookie.httpOnly);
			encoder.addCookie(c);
			nettyResponse.addHeader(SET_COOKIE, encoder.encode());
		}

		if (!response.headers.containsKey(CACHE_CONTROL)) {
			nettyResponse.setHeader(CACHE_CONTROL, "no-cache");
		}

	}

	protected static void writeResponse(ChannelHandlerContext ctx,
			Response response, HttpResponse nettyResponse,
			HttpRequest nettyRequest) {
		byte[] content = null;

		final boolean keepAlive = isKeepAlive(nettyRequest);
		if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
			content = new byte[0];
		} else {
			content = response.out.toByteArray();
		}

		ChannelBuffer buf = ChannelBuffers.copiedBuffer(content);
		nettyResponse.setContent(buf);
		setContentLength(nettyResponse, response.out.size());

		ChannelFuture f = ctx.getChannel().write(nettyResponse);

		if (!keepAlive) {
			f.addListener(ChannelFutureListener.CLOSE);
		}
	}

	public static void copyResponse(ChannelHandlerContext ctx, Request request,
			Response response, HttpRequest nettyRequest) throws Exception {
		// response.out.flush();

		// Decide whether to close the connection or not.

		HttpResponse nettyResponse = new DefaultHttpResponse(
				HttpVersion.HTTP_1_1,
				HttpResponseStatus.valueOf(response.status));
		if (exposePlayServer) {
			nettyResponse.setHeader(SERVER, signature);
		}

		if (response.contentType != null) {
			nettyResponse.setHeader(
							CONTENT_TYPE,
							response.contentType
									+ (response.contentType.startsWith("text/")
											&& !response.contentType
													.contains("charset") ? "; charset=utf-8"
											: ""));
		} else {
			nettyResponse.setHeader(CONTENT_TYPE, "text/plain; charset=utf-8");
		}

		addToResponse(response, nettyResponse);

		final Object obj = response.direct;
		File file = null;
		ChunkedInput stream = null;
		InputStream is = null;
		if (obj instanceof File) {
			file = (File) obj;
		} else if (obj instanceof InputStream) {
			is = (InputStream) obj;
		} else if (obj instanceof ChunkedInput) {
			// Streaming we don't know the content length
			stream = (ChunkedInput) obj;
		}

		final boolean keepAlive = isKeepAlive(nettyRequest);
		if (file != null && file.isFile()) {
			try {
				if (nettyResponse.getStatus().equals(
						HttpResponseStatus.NOT_MODIFIED)) {

					Channel ch = ctx.getChannel();

					// Write the initial line and the header.
					ChannelFuture writeFuture = ch.write(nettyResponse);

					if (!keepAlive) {
						// Close the connection when the whole content is
						// written out.
						writeFuture.addListener(ChannelFutureListener.CLOSE);
					}
				} else {
					nettyResponse.setHeader(CONTENT_TYPE, MimeTypes
							.getContentType(file.getName(), "text/plain"));
					final RandomAccessFile raf = new RandomAccessFile(file, "r");
					try {
						long fileLength = raf.length();

						if (!nettyResponse.getStatus().equals(
								HttpResponseStatus.NOT_MODIFIED)) {
							logger.trace("file length is [" + fileLength + "]");
							setContentLength(nettyResponse, fileLength);
						}

						Channel ch = ctx.getChannel();

						// Write the initial line and the header.
						ChannelFuture writeFuture = ch.write(nettyResponse);

						// Write the content.
						// If it is not a HEAD
						if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)) {
							writeFuture = ch.write(new ChunkedFile(raf, 0,
									fileLength, 8192));
						} else {
							raf.close();
						}
						if (!keepAlive) {
							// Close the connection when the whole content is
							// written out.
							writeFuture
									.addListener(ChannelFutureListener.CLOSE);
						}
					} catch (Throwable exx) {
						try {
							raf.close();
						} catch (Throwable ex) { /* Left empty */
						}
						try {
							ctx.getChannel().close();
						} catch (Throwable ex) { /* Left empty */
						}
					}

				}
			} catch (Exception e) {
				throw e;
			}
		} else if (is != null) {
			ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
			if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
					&& !nettyResponse.getStatus().equals(
							HttpResponseStatus.NOT_MODIFIED)) {
				writeFuture = ctx.getChannel().write(new ChunkedStream(is));
			} else {
				is.close();
			}
			if (!keepAlive) {
				writeFuture.addListener(ChannelFutureListener.CLOSE);
			}
		} else if (stream != null) {
			ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
			if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
					&& !nettyResponse.getStatus().equals(
							HttpResponseStatus.NOT_MODIFIED)) {
				writeFuture = ctx.getChannel().write(stream);
			} else {
				stream.close();
			}
			if (!keepAlive) {
				writeFuture.addListener(ChannelFutureListener.CLOSE);
			}
		} else {
			writeResponse(ctx, response, nettyResponse, nettyRequest);
		}
		
		ACLogger.log(request, response, nettyResponse.getHeader(Header.Names.CONTENT_LENGTH));
		Perf4j.stop("fh", "处理结束");
	}

	public static boolean isModified(long last,
			HttpRequest nettyRequest) {

		if (nettyRequest.containsHeader(IF_MODIFIED_SINCE)) {
			final String ifModifiedSince = nettyRequest
					.getHeader(IF_MODIFIED_SINCE);

			if (!StringUtil.isEmpty(ifModifiedSince)) {
				try {
					Date browserDate = Time.httpFormatter.parse(
							ifModifiedSince);
					if (browserDate.getTime() >= last) {
						return false;
					}
				} catch (ParseException ex) {
					logger.warn("Can't parse HTTP date", ex);
				}
				return true;
			}
		}
		return true;
	}


	public static boolean isKeepAlive(HttpMessage message) {
		return HttpHeaders.isKeepAlive(message)
				&& message.getProtocolVersion().equals(HttpVersion.HTTP_1_1);
	}

	public static void setContentLength(HttpMessage message, long contentLength) {
		message.setHeader(HttpHeaders.Names.CONTENT_LENGTH,
				String.valueOf(contentLength));
	}
	
	 // ~~~~~~~~~~~ Websocket
    final static Map<ChannelHandlerContext, Inbound> channels = new ConcurrentHashMap<ChannelHandlerContext, Inbound>();

    private void websocketFrameReceived(final ChannelHandlerContext ctx, WebSocketFrame webSocketFrame) {
        Inbound inbound = channels.get(ctx);
        if (webSocketFrame.isBinary()) {
            inbound._received(new slap.http.websocket.WebSocketFrame(webSocketFrame.getBinaryData().array()));
        } else {
            inbound._received(new slap.http.websocket.WebSocketFrame(webSocketFrame.getTextData()));
        }
    }

    private void websocketHandshake(final ChannelHandlerContext ctx, HttpRequest req, MessageEvent e) throws Exception {
    	
        // Create the WebSocket handshake response.
        HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
        res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);
        res.addHeader(CONNECTION, HttpHeaders.Values.UPGRADE);

        // Fill in the headers and contents depending on handshake method.
        if (req.containsHeader(SEC_WEBSOCKET_KEY1) && req.containsHeader(SEC_WEBSOCKET_KEY2)) {
            // New handshake method with a challenge:
            res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
            res.addHeader(SEC_WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri());
            String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
            if (protocol != null) {
                res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
            }

            // Calculate the answer of the challenge.
            String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
            String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
            int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
            int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
            long c = req.getContent().readLong();
            ChannelBuffer input = ChannelBuffers.buffer(16);
            input.writeInt(a);
            input.writeInt(b);
            input.writeLong(c);
            try {
                ChannelBuffer output = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));
                res.setContent(output);
            } catch (NoSuchAlgorithmException ex) {
                throw new UnexpectedException(ex);
            }
        } else {
            // Old handshake method with no challenge:
            res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
            res.addHeader(WEBSOCKET_LOCATION, "ws://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri());
            String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
            if (protocol != null) {
                res.addHeader(WEBSOCKET_PROTOCOL, protocol);
            }
        }

        // Keep the original request
        Request request = new RequestNettyImpl(req, ctx);

        //TODO Route the websocket request
        //request.method = "WS";
        
        // Upgrade the connection and send the handshake response.
        ChannelPipeline p = ctx.getChannel().getPipeline();
        p.remove("aggregator");
        p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());

        // Connect
        ctx.getChannel().write(res);

        p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
        req.setMethod(new HttpMethod("WEBSOCKET"));

        // Inbound
        final Inbound inbound = new Inbound() {

            @Override
            public boolean isOpen() {
                return ctx.getChannel().isOpen();
            }
        };
        channels.put(ctx, inbound);

        // Outbound
        Outbound outbound = new Outbound() {

            final List<ChannelFuture> writeFutures = Collections.synchronizedList(new ArrayList<ChannelFuture>());
            Promise<Void> closeTask;

            synchronized void writeAndClose(ChannelFuture writeFuture) {
                if (!writeFuture.isDone()) {
                    writeFutures.add(writeFuture);
                    writeFuture.addListener(new ChannelFutureListener() {

                        public void operationComplete(ChannelFuture cf) throws Exception {
                            writeFutures.remove(cf);
                            futureClose();
                        }
                    });
                }
            }

            void futureClose() {
                if (closeTask != null && writeFutures.isEmpty()) {
                    closeTask.invoke(null);
                }
            }

            @Override
            public void send(String data) {
                if (!isOpen()) {
                    throw new IllegalStateException("The outbound channel is closed");
                }
                writeAndClose(ctx.getChannel().write(new DefaultWebSocketFrame(data)));
            }

            @Override
            public void send(byte opcode, byte[] data, int offset, int length) {
                if (!isOpen()) {
                    throw new IllegalStateException("The outbound channel is closed");
                }
                writeAndClose(ctx.getChannel().write(new DefaultWebSocketFrame(opcode, wrappedBuffer(data, offset, length))));
            }

            @Override
            public synchronized boolean isOpen() {
                return ctx.getChannel().isOpen() && closeTask == null;
            }

            @Override
            public synchronized void close() {
                closeTask = new Promise<Void>();
                closeTask.onRedeem(new Procedure1<Promise<Void>>() {

                    public void invoke(Promise<Void> completed) {
                        writeFutures.clear();
                        ctx.getChannel().disconnect();
                        closeTask = null;
                    }
                });
                futureClose();
                inbound.handler.onClose();
            }
        };

        Invoker.invoke(new WebSocketInvocation(request, inbound, outbound, ctx, e));
    }
    
	@Override
	public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e)
			throws Exception {
		 channelGroup.add(ctx.getChannel());
	}

	@Override
    public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
        Inbound inbound = channels.get(ctx);
        if (inbound != null) {
            inbound.close();
        }
        channels.remove(ctx);
        channelGroup.remove(ctx.getChannel());
    }

   
}
